import gsap from 'gsap'
import { Draggable } from 'gsap/Draggable'

interface HelperProps {
  containerEl: HTMLElement
  listEl: HTMLElement
  thumbEl: HTMLElement

  onChange: (value: number) => void
}

const SCROLL_DURATION_PERTIME = 1 // second

export default class Helper {
  containerEl: HTMLElement
  listEl: HTMLElement
  thumbEl: HTMLElement

  scrolling = false
  pressing = false
  containerHeight = 0
  containerTop = 0
  containerScrollTop = 0
  listHeight = 0
  itemHeight = 0
  thumbHeight = 0
  thumbOffsetTop = 0
  scrollTop = 0

  rafId: null | number = null

  active: number | null = null
  onChange: HelperProps['onChange']

  constructor({ containerEl, listEl, thumbEl, onChange }: HelperProps) {
    this.containerEl = containerEl
    this.listEl = listEl
    this.thumbEl = thumbEl

    this.active = null
    this.onChange = onChange

    this.adjust()

    this.updatePosition()
    this.settle()
  }

  setActive(active: number) {
    this.scrollTo(active)
    this.active = active
    this.onChange?.(active)
  }

  adjust() {
    this.containerHeight = this.containerEl.offsetHeight
    this.itemHeight = Math.round(this.listEl.children?.[0]?.clientHeight) || 0
    this.thumbEl.style.height = `${this.itemHeight}px`

    const adjustedHeight = this.itemHeight * Math.floor(this.containerHeight / this.itemHeight)
    this.containerEl.style.height = adjustedHeight + 'px'
  }

  updatePosition() {
    this.containerHeight = this.containerEl.offsetHeight
    this.listHeight = this.listEl.offsetHeight
    this.thumbHeight = this.thumbEl.offsetHeight
    this.itemHeight = Math.round(this.listEl.children?.[0].getBoundingClientRect().height) || 0

    this.containerTop = Math.round(this.containerEl.getBoundingClientRect().top)
    this.containerScrollTop = this.containerEl.offsetTop
    this.thumbOffsetTop = Math.round(this.thumbEl.getBoundingClientRect().top) - this.containerTop
    this.scrollTop = this.containerEl.scrollTop
  }

  createDragable() {
    const _this = this
    Draggable.create(this.thumbEl, {
      type: 'y',
      bounds: this.containerEl,
      inertia: true,
      lockAxis: true,
      liveSnap: function (value) {
        return Math.round(value / _this.itemHeight) * _this.itemHeight
      },
      onPress() {
        _this.pressing = true
        setTimeout(() => {
          _this.check()
        }, 500)
      },
      onDrag() {
        _this.check()
      },
      onRelease() {
        _this.pressing = false
        _this.dispose()

        setTimeout(
          () => {
            _this.settle()
          },
          _this.scrolling ? SCROLL_DURATION_PERTIME * 1000 : 0
        )
      },
    })
  }

  settle() {
    this.updatePosition()

    const scrollTop = this.containerEl.scrollTop || 0
    const idx = Math.ceil((this.thumbOffsetTop + scrollTop) / this.itemHeight)
    if (this.active !== idx) {
      this.active = idx
      this.onChange?.(idx)
    }
  }

  check() {
    if (!this.pressing) return

    this.updatePosition()
    if (this.thumbOffsetTop + this.itemHeight > this.containerHeight - 20) {
      setTimeout(() => {
        this.scrollDown()
      }, 500)
    } else if (this.thumbOffsetTop < this.itemHeight) {
      setTimeout(() => {
        this.scrollUp()
      }, 500)
    } else {
      this.dispose()
    }
  }

  scrollDown() {
    if (this.rafId || !this.pressing) return

    const _this = this
    function task() {
      if (_this.scrollTop < _this.listHeight - _this.containerHeight) {
        _this.scrolling = true
        _this.scrollTop += _this.itemHeight
        gsap.to(_this.containerEl, {
          duration: SCROLL_DURATION_PERTIME,
          scrollTo: _this.scrollTop,
          onComplete() {
            _this.scrolling = false
          },
        })
      } else {
        _this.dispose()
      }
      _this.rafId = requestAnimationFrame(task)
    }
    _this.rafId = requestAnimationFrame(task)
  }

  scrollUp() {
    if (this.rafId || !this.pressing) {
      return
    }
    const _this = this
    function task() {
      if (_this.scrollTop > 40) {
        _this.scrolling = true
        _this.scrollTop -= _this.itemHeight
        gsap.to(_this.containerEl, {
          duration: SCROLL_DURATION_PERTIME,
          scrollTo: _this.scrollTop,
        })
      } else if (_this.scrollTop > 0) {
        _this.scrolling = true
        _this.scrollTop = 0
        gsap.to(_this.containerEl, {
          duration: SCROLL_DURATION_PERTIME,
          scrollTo: _this.scrollTop,
        })
      } else {
        _this.dispose()
      }
      _this.rafId = requestAnimationFrame(task)
    }
    _this.rafId = requestAnimationFrame(task)
  }

  scrollTo(idx: number) {
    const distance = idx * this.itemHeight

    const _this = this
    function task() {
      if (distance < _this.listHeight - _this.containerHeight) {
        _this.scrolling = true
        _this.scrollTop = distance
        gsap.to(_this.containerEl, {
          duration: 0,
          scrollTo: distance,
          onComplete() {
            _this.scrolling = false
            _this.rafId = null
          },
        })
      } else {
        _this.dispose()
      }
    }
    _this.rafId = requestAnimationFrame(task)
  }

  dispose() {
    if (this.rafId) {
      cancelAnimationFrame(this.rafId)
      this.rafId = null
    }
  }
}
